Aula 1

Nesta aula apresentaremos noções básicas da linguagem Scala. Nos exemplos abaixo, abordaremos:

  • Valores e Variáveis
  • Tipos
  • Controle de fluxo
  • Loops
  • Funções
  • Objetos
  • Classes

Para espantar a má sorte, comecemos com o básico:


In [ ]:
print("Olá Mundo!")

Valores e Variáveis


Em Scala, nós podemos armazenar informações em valores e variáveis.

Valores

Valores são informações imutáveis, ou seja, constantes. No exemplo abaixo, criamos um valor e atribuímos 10 a ele:


In [ ]:
val valor = 10

Como valores são imutáveis, não podemos realizar uma segunda atribuição nele. Quando tentamos atribuir uma nova informação, obtemos o erro de reassigment to val:


In [ ]:
valor = 20

Variáveis

Variáveis são informações mutáveis, ou seja, como o próprio nome sugere, variáveis. No exemplo abaixo, criamos uma variável e atribuímos 10 a ela:


In [ ]:
var variavel = 10

Diferente dos valores, podemos alterar a informação da variável:


In [ ]:
variavel = 20
print(variavel)

Porém, nunca podemos mudar o tipo da variável. Caso o façamos, obtemos um erro de type mismatch.


In [ ]:
variavel = "string"

Quando vamos atribuir algo a um val ou var, podemos escrever um bloco de código. Blocos de código podem retornar informações em Scala.


In [ ]:
val x = 1
val y = {
    val a = 10
    a + x
}

Tipos


Assim como Java, Scala possui alguns tipos básicos de informações. Para definir o tipo de um valor ou variável, basta usar a seguinte sintaxe:


In [ ]:
var x: Int = 10

O tipo é definido durante a primeira atribuição. Como em valores realizamos apenas uma atribuição, não é necessário colocar tipo, porém, se quisermos, podemos forçar uma variável a ser de um certo tipo:


In [ ]:
var inteiro: Int
inteiro = "string"

Os seguintes tipos são os mais utilizados em Scala:

  • Int - inteiro
  • Double - decimal
  • Boolean - booleano
  • Char - caractere
  • String - cadeia de caracteres

In [ ]:
val inteiro = 1
val decimal = 1.5
val booleano = true
val char = 'c'
val string = "string"

O tipo Any

Em Scala, existe um tipo que representa qualquer coisa: Any. Uma variável desse tipo pode receber qualquer informação.


In [ ]:
var any:Any = 1
any = 1.5
any = true
any = 'c'
any = "string"

Arrays

Arrays em Scala são, como em Java, vetores de tamanho fixo de um determinado tipo. Podemos declarar um Array das seguintes maneiras:


In [ ]:
// Um array de dez inteiros, inicializado com zeros
val nums = new Array[Int](10)

// Um array de string, inicializado com nulls
val a = new Array[String](10)

// Podemos também definir um array informando seus conteúdos ao invés de tamanho
val s = Array("Hello", "World")
// OBS: qunado informa-see os elementos do array, não utilizamos new

// Em Scala, utilizamos () ao invés de [] para acessar os elementos de um array.
s(0) = "Goodbye"

Quando inicializamos um array com mais de um tipo de conteúdo, seu tipo é definido por uma classe mais genérica:


In [ ]:
val numeros = Array(1,2.0,3e-2)

val misturado = Array(1,'a',2.0,"bc")

Para definir arrays multidimensionais, precisamos especificar suas dimensões.


In [ ]:
val matriz2d = Array.ofDim[Int](2,2)
val matriz3d = Array.ofDim[Int](2,2,2)

Controle de fluxo


A sintaxe para controle de fluxo é igual a de linguagens como C, C++ e Java.


In [ ]:
val x = 11

if(x%2==0){
    print(x+ " é par")
}
else{
    print(x+ " é ímpar")
}

Como Scala é funcional, podemos usar controle de fluxo para retornar valores:


In [ ]:
val x = 11
var paridade = if(x%2==0) "par" else "ímpar"
print("x é "+paridade)

Loops


Loops em Scala são semelhantes à Java. Porém, Scala conta com um loop do tipo for mais poderoso que as demais linguagens.

For

Todo for em Scala itera alguma sequência. Para percorrer intervalos de valores, podemos utilizar os geradores until e to .


In [ ]:
println("for com until")
for(i <- 1 until 10){
    print(i+" ")
}

println('\n')

println("for com to")
for(i <- 1 to 10){
    print(i+" ")
}

Quando precisamos fazer laços aninhados (um loop dentro de outro) normalmente utilizamos 2 for diferentes.


In [ ]:
for(i <- 1 to 3)
    for(j <- 1 to 3)
        print("("+i+","+j+") ")

Em Scala, podemos usar for comprehensions. Em uma única definição de for podemos definir a combinação de laços na qual o bloco de comando será executado:


In [ ]:
for{
    i <- 1 to 3
    j <- 1 to 3
}{
    print("("+i+","+j+") ")
}

Essa compressão também permite definirmos condições sobre os valores do loop.


In [ ]:
for{
    i <- 1 to 3
    if i%2==1
    j <- 1 to 3
    if(j>=i)
}{
    print("("+i+","+j+") ")
}

Como o for itera sobre uma sequência, podemos fazê-lo iterar sobre um Array


In [ ]:
val pares = Array(2,4,6,8)

for(par <- pares){
    print(par+" ")
}

Gerando arrays com for

Como vimos anteriormente, blocos de comando podem gerar valores, desque estes sejam a última informação do bloco. Scala contém um operador chamado yield que permite que o bloco do for retorne uma estrutura análoga ao array: um IndexedSeq. Por hora, podemos tratar essa estrutura como um array.


In [ ]:
val pares = for{
    i <- 0 to 10
    if i%2 == 0
} yield {
    i
}

While

Em Scala, o laço do tipo while é análogo às outras linguagens:


In [ ]:
var i = 1
while(i<=10){
    print(i+" ")
    i = i + 1
}

Do While

Assim como o while, o laço do tipo do while é análogo às outras linguagens:


In [ ]:
var i = 0
do {
    print(i+" ")
    i = i+1
}while(i<10)

Funções


Em Scala, diferente de Java, além de métodos nós temos funções. A lógica para definir uma função é simples: começamos com def seguido pelo nome da função, em seguida apresentamos os parâmetros e seus tipos e, por fim, o tipo do retorno da função.


In [ ]:
def soma1(x: Int): Int = {
    return x + 1
}

print(soma1(10))

Particularidades:

Uma função que não tem retorno é do tipo Unit (semelhante ao void em Java).


In [ ]:
def mostra(x: Any): Unit = {
    println(x)
}

mostra(10)

Assim como em estruturas de controle e laços, não precisamos colocar o código da função entre parênteses quando for uma função de apenas uma linha de código.


In [ ]:
def soma1(x: Int): Int = return x+1
print(soma1(10))

Não precisamos utilizar return para retornar um valor, basta que esse valor seja escrito no fim da função.


In [ ]:
def soma1(x: Int): Int = x+1
print(soma1(10))

Não precisamos determinar o tipo do retorno de uma função, pois o compilador é capaz de inferir esse tipo.


In [ ]:
def soma1(x: Int) = x+1
print(soma1(10))

Quando uma função pode ter mais de um tipo de retorno, o compilador define o retorno da função como um tipo intermediário.


In [ ]:
def f(x: Int) = if(x>0) 1 else 0.0

val x = f(0)
val y = f(1)

In [ ]:
def f(x: Int) = if(x>0) 1 else "menor ou igual à 0"

val x = f(0)
val y = f(1)

Em funções recursivas, é necessário definir o tipo, pois é uma operação muito custosa inferir todas as possibilidades de retorno de uma função recursiva.


In [ ]:
def mostraRecursivo(x: Int) = {
    if(x>0){
        print(x+", ")
        mostraRecursivo(x-1)
    }
    else print(x)
}

mostraRecursivo(10)

In [ ]:
def mostraRecursivo(x: Int): Unit = {
    if(x>0){
        print(x+", ")
        mostraRecursivo(x-1)
    }
    else print(x)
}

mostraRecursivo(10)

Objetos


Um objeto é uma estrutura que carrega consigo informações(atributos) e comportamentos(métodos). Objetos são únicos e não podem ser sobrescritos.

Podemos escrever um objeto em Scala como um código qualquer. Nesse código:

  • as variáveis e os valores são atributos
  • as funções são métodos

Para definir um objeto em Scala basta utilizar a seguinte sintaxe:


In [ ]:
object Contador{ //nome do objeto
    
    var numero = 10 //um atributo que carrega a informação de um número inteiro
    
    def valor = numero //um método que retorna o valor de "numero"
    
    def tick = { //um método que decrementa o valor de número
        numero -= 1
    }
    
    def reset = { //um método que reseta o valor de "numero" para 10 
        numero = 10
    }
}

println(Contador.valor)
Contador.tick
println(Contador.valor)
Contador.tick
println(Contador.valor)

Contador.reset
println(Contador.valor)

Classes


Grupo de objetos com os mesmos atributos e os mesmos comportamentos pertencem à mesma classe.

Diferente de um objeto, uma classe precisa ser atribuída a um valor ou variável para poder ser utilizada.

Criar uma classe em Scala é bastante similar à criação de classes em outras python:


In [ ]:
class Pessoa {
    var nome: String = null
    var cpf: String = null
}

val mario = new Pessoa

mario.nome = "Mario"
mario.cpf = "060.000.000-00"

println(mario.nome)
println(mario.cpf)

OBS: Por padrão, todos os métodos e atributos de uma classe em Scala são públicos. Para defini-los como privados, basta utilizar o modificador private

OBS2: Atributos definidos como val são apenas para leitura, enquanto os definidos como var são para leitura e escrita

Representando uma instância como string

Podemos definir um método chamado toString para que, quando chamarmos a função print, seja feita uma apresentação mais legível do objeto instanciado.

Para implementar esse método em Scala, precisamos fazer uma sobrescrita, tendo de adicionar o modificador override antes do nome do método.


In [ ]:
class Pessoa(nome: String, cpf: String){
    def this(nome: String) = this(nome, "Não cadastrado")
    
    def getNome = nome
    def getCPF = cpf
    
    override def toString = "Nome: "+nome+", CPF: "+cpf
}

val mario = new Pessoa("Mário")
print(mario)

Operadores

Scala permite a definição de operações entre instâncias da classe e outros objetos. Em Scala, todas as informações são objetos e suas operações são chamadas de métodos:


In [ ]:
val x = 10

//podemos chamar um método como uma operação, usando uma notação mais limpa
println(x + 10)
//e podemos também chamar um método pela notação padrão, utilizando ponto + nome do método + argumentos
println(x.+(10))

Para exemplificar o uso de operadores, vamos definir uma classe que representa os números Racionais em forma de fração, definindo métodos que nos permitem operar entre eles:


In [ ]:
class Racional(n: Int, d: Int){
    //declaramos essas variáveis para tornar essas informações como públicas
    //utilizamos val para evitar sobrescrita
    val numerador = n
    val denominador = d    
    
    def somar(b: Racional): Racional = 
        new Racional(numerador*b.denominador + b.numerador * denominador, denominador*b.denominador)
    
    def subtrair(b: Racional): Racional = 
        new Racional(numerador*b.denominador - b.numerador * denominador, denominador*b.denominador)
    
    override def toString: String = numerador.toString+"/"+denominador.toString
}

val metade = new Racional(1,2)
val terco = new Racional(1,3)

println("soma: "+metade.somar(terco))
println("subtração: "+metade.subtrair(terco))

Podemos reescrever esses métodos como os seguintes operadores: + e -:


In [ ]:
class Racional(n: Int, d: Int){
    //declaramos essas variáveis para tornar essas informações como públicas
    //utilizamos val para evitar sobrescrita
    val numerador = n
    val denominador = d    
    
    def + (b: Racional): Racional = 
        new Racional(numerador*b.denominador + b.numerador * denominador, denominador*b.denominador)
    
    def - (b: Racional): Racional = 
        new Racional(numerador*b.denominador - b.numerador * denominador, denominador*b.denominador)
    
    override def toString: String = numerador.toString+"/"+denominador.toString
}

val metade = new Racional(1,2)
val terco = new Racional(1,3)

println("soma: "+(metade + terco))
println("subtração: "+(metade - terco))

Exercícios


1. Escreva uma função (de preferência, recursiva) que receba um inteiro n retorna o n-ésimo número de Fibonnaci


In [ ]:
def fib(n: Int): Int =
    if(n == 0) 0
    else if (n == 1) 1
    else fib(n-1) + fib(n-2)

for(i <- 0 to 10) print(fib(i)+" ")

2. Escreva uma função que gere todas as peças de um Dominó


In [ ]:
def domino: Unit = 
    for{
        i <- 0 to 6
        j <- i to 6
    } print(s"($i,$j) ")

domino

3. Escreva uma classe que represente uma matriz m x n que tenha os seguintes métodos:

  • Criar uma matriz informando suas dimensões (m x n);
  • Acessar o elemento da matriz dada uma coordenada;
  • Alterar o elemento da matriz dada uma coordenada;
  • Imprimir a matriz na tela

Implemente também as seguintes operações:

  • Soma
  • Subtração

In [ ]:
class Matrix(m: Int, n: Int){

    def this(n: Int) = this(n,n)
    
    val matrix = Array.ofDim[Double](m,n)
    
    def get(i: Int, j: Int): Double = matrix(i)(j)

    //O método apply() permite que o objeto seja chamado como uma função.
    //Como estamos implementando uma matriz, seria interessante que possamos acessar
    //seus valores fazendo uma chamada como A(i,j).
    def apply(i: Int, j: Int): Double = get(i,j)
    
    def set(i: Int, j: Int, x: Double): Unit = 
        matrix(i)(j) = x
    
    //O método update() é análogo ao apply, porém ele serve apra atribuições.
    //Para a matriz, é interessante que possamos alterar o valor de um elemento direto pelo índice,
    //como A(i,j) = 10.0 .
    def update(i: Int, j: Int, x: Double): Unit = set(i,j,x)
    
    def +(b: Matrix): Matrix = {
        val c = new Matrix(m,n)
        
        for{
            i <- 0 until m
            j <- 0 until n
        } c(i,j) = this(i,j) + b(i,j)
        
        c
    }
    
    def -(b: Matrix): Matrix = {
        val c = new Matrix(m,n)
        
        for{
            i <- 0 until m
            j <- 0 until n
        } c(i,j) = this(i,j) - b(i,j)
        
        c
    }
    
    override def toString(): String = {
        var result = ""
        
        for(i <- 0 until m){
            result += "["+matrix(i).mkString(",")+"]\n"
        }
        
        result
    }
}


val m1 = new Matrix(2)
m1(0,0) = 10

val m2 = new Matrix(2)
m2(1,1) = 20

println(m1+m2)
println(m1-m2)

4. Crie um Object para gerar matrizes preenchidas automaticamente(como a matriz identidate, matriz diagonal, etc.)


In [ ]:
object MatrixHelper{
    
    def generateDiagonal(n: Int, x: Double): Matrix = {
        val I = new Matrix(n)
        for (i <- 0 until n) I(i,i) = x
        I
    }
    
    //Uma matriz identidade é uma matriz diagonal preenchida com 1's !
    def generateIdentity(n: Int): Matrix = 
        generateDiagonal(n, 1)
}

println(s"Matriz diagonal (2,2) com 5:\n${MatrixHelper.generateDiagonal(2,5)}\n")
println(s"Matriz identidade (3,3):\n${MatrixHelper.generateIdentity(3)}")